fix(operator): add 100MB memory limit to binary file scan to prevent OOM#6811
fix(operator): add 100MB memory limit to binary file scan to prevent OOM#6811roshiiiz wants to merge 14 commits into
Conversation
…nfigured operators
- reformat the over-100-column require() in AsterixDBSourceOpDesc so scalafmtCheckAll passes - align the scan-source missing-fileName error in both LogicalPlan copies (amber + workflow-compiling-service) with the new 'No file selected...' wording that the updated specs expect - WorkflowCompiler: keep the first error recorded per operator so the root-cause resolution failure (e.g. FileNotFoundException with the bad path) is not overwritten by follow-on schema-propagation failures on the same operator - update workflow-operator DescSpecs that pinned the old contracts: sourceSchema() returning null before configuration (CSVOld/JSONL/MySQL/PostgreSQL/AsterixDB/SQL) and ArrowSourceOpDesc.inferSchema throwing a bare IOException
Automated Reviewer SuggestionsBased on the
|
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #6811 +/- ##
============================================
- Coverage 78.69% 78.57% -0.13%
+ Complexity 3731 3728 -3
============================================
Files 1161 1161
Lines 46066 46081 +15
Branches 5106 5110 +4
============================================
- Hits 36253 36207 -46
- Misses 8208 8260 +52
- Partials 1605 1614 +9
*This pull request uses carry forward flags. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
| config | throughput | MB/s | latency | max Δ latest / 7d | |
|---|---|---|---|---|---|
| 🔴 | bs=10 sw=10 sl=64 | 357 | 0.218 | 26,621/41,410/41,410 us | 🔴 +39.0% / 🔴 +152.9% |
| ⚪ | bs=100 sw=10 sl=64 | 798 | 0.487 | 126,190/149,642/149,642 us | ⚪ within ±5% / 🔴 +36.9% |
| ⚪ | bs=1000 sw=10 sl=64 | 914 | 0.558 | 1,095,781/1,127,775/1,127,775 us | ⚪ within ±5% / 🔴 +8.8% |
Baseline details
Latest main b4d1865 from same runner
| config | metric | PR | latest main | 7d avg | Δ latest | Δ 7d |
|---|---|---|---|---|---|---|
| bs=10 sw=10 sl=64 | throughput | 357 tuples/sec | 430 tuples/sec | 759.72 tuples/sec | -17.0% | -53.0% |
| bs=10 sw=10 sl=64 | MB/s | 0.218 MB/s | 0.262 MB/s | 0.464 MB/s | -16.8% | -53.0% |
| bs=10 sw=10 sl=64 | p50 | 26,621 us | 22,271 us | 12,653 us | +19.5% | +110.4% |
| bs=10 sw=10 sl=64 | p95 | 41,410 us | 29,800 us | 16,371 us | +39.0% | +152.9% |
| bs=10 sw=10 sl=64 | p99 | 41,410 us | 29,800 us | 19,941 us | +39.0% | +107.7% |
| bs=100 sw=10 sl=64 | throughput | 798 tuples/sec | 814 tuples/sec | 966.78 tuples/sec | -2.0% | -17.5% |
| bs=100 sw=10 sl=64 | MB/s | 0.487 MB/s | 0.497 MB/s | 0.59 MB/s | -2.0% | -17.5% |
| bs=100 sw=10 sl=64 | p50 | 126,190 us | 121,252 us | 103,654 us | +4.1% | +21.7% |
| bs=100 sw=10 sl=64 | p95 | 149,642 us | 146,292 us | 109,308 us | +2.3% | +36.9% |
| bs=100 sw=10 sl=64 | p99 | 149,642 us | 146,292 us | 116,369 us | +2.3% | +28.6% |
| bs=1000 sw=10 sl=64 | throughput | 914 tuples/sec | 941 tuples/sec | 997.95 tuples/sec | -2.9% | -8.4% |
| bs=1000 sw=10 sl=64 | MB/s | 0.558 MB/s | 0.574 MB/s | 0.609 MB/s | -2.8% | -8.4% |
| bs=1000 sw=10 sl=64 | p50 | 1,095,781 us | 1,061,660 us | 1,007,348 us | +3.2% | +8.8% |
| bs=1000 sw=10 sl=64 | p95 | 1,127,775 us | 1,104,414 us | 1,050,149 us | +2.1% | +7.4% |
| bs=1000 sw=10 sl=64 | p99 | 1,127,775 us | 1,104,414 us | 1,079,497 us | +2.1% | +4.5% |
Raw CSV
config_idx,batch_size,schema_width,string_len,num_batches,total_ms,total_tuples,total_bytes,tuples_per_sec,mb_per_sec,lat_p50_us,lat_p95_us,lat_p99_us
0,10,10,64,20,560.01,200,128000,357,0.218,26620.63,41409.75,41409.75
1,100,10,64,20,2507.69,2000,1280000,798,0.487,126190.35,149642.17,149642.17
2,1000,10,64,20,21892.72,20000,12800000,914,0.558,1095781.32,1127775.45,1127775.452e58992 to
796f392
Compare
Great question! While the theoretical Java byte limit is 2GB, ByteArrayOutputStream doubles its internal buffer capacity every time it resizes. This means reading a 1GB file actually requires >3GB of free heap space just to perform the array copy. On standard local deployments, this instantly triggers OutOfMemoryErrors or severe GC death spirals that crash the worker JVM before it ever hits the 2GB limit. The 100MB limit was chosen as a conservative safety net to guarantee the worker never crashes, while actively nudging users to select the large binary attribute type instead for bigger files (since large binary streams the data chunk-by-chunk and creates zero heap pressure). However, I am completely open to bumping this limit to 500MB if you feel that's a better threshold for standard use cases! Let me know what limit you'd prefer and I'll gladly update the PR. |
|
Do you think producing OOM is a bug? I think it's natural to produce OOM error if the operator doesn't support reading more than 2GB file. I am okay if the direction is to make the error message user-friendly and guide to using |
I completely understand your point, and I agree that an OOM is natural when hitting that 2GB limit. |
|
If you want a faster fail path, then I wonder why we have to run the operator to check the file size. Did you consider a way to check the file size before running the operator? Also, exceeding 100MB doesn't necessarily mean the operator will cause an OOM. Can you reconsider the idea? |
That is a great suggestion, but checking the file size upfront introduces some edge cases. For instance, if a user uploads a .zip file, we cannot determine the uncompressed size of the extracted entries until the operator is actively streaming and unzipping them on the fly. (I might be overthinking this as well) However, I completely agree that your original suggestion is 100% the cleanest and best approach. I will go ahead and remove the threshold entirely. I'll update the PR to let it hit the Java array limit, and we can simply catch the resulting OutOfMemoryError or IllegalArgumentException and wrap it in user-friendly large-binary hint. Thanks for the feedback I will update the PR with this approach shortly. |
|
Sounds good! I haven't thought about a zip file but your opinion makes sense that it's not possible to cover all the cases and check the file size beforehand. You can go ahead with the fix. |
Do you have some data to support this? For example, can you test the maximum possible file size for this operator? We want to set this limit to upper bound instead of lower bound. |
Thanks for the feedback. I actually had a discussion with @kunwp1 in the thread above relating somewhat to this exact concern. We agreed that instead of trying to find the perfect artificial upper bound, it is much cleaner to just remove the hardcoded threshold entirely. I am updating the PR to let the operator naturally hit the Java array limit on its own, and we can simply catch the resulting OutOfMemoryError or IllegalArgumentException and wrap it in the user-friendly large-binary hint. |
Nope, I can confirm this PR is completely unrelated to #3721. My frontend changes just strip technical exception prefixes from operator warnings in the global error handler so it doesn't touch the login/registration notification durations at all. |
509246d to
0386079
Compare
c0793ce to
861c761
Compare
The description mentioned #3721. Please fix it. |
What changes were proposed in this PR?
This PR introduces a strict 100 MB memory allocation limit when scanning files using the standard
binaryattribute type.Why is it needed?
Previously, when users mistakenly attempted to read massive files (e.g., >8GB) using the
binaryattribute type (instead of the streaminglarge binarytype), theByteArrayOutputStreamwould attempt to allocate the entire file into a single contiguous Java byte array. Since standardbyte[]arrays max out at ~2.14GB, this inherently fails. Worse, on standard developer laptops or lower-memory deployments, this massive allocation attempt triggered immediateIllegalArgumentExceptions or severe JVM Garbage Collection death spirals, causing thecomputing-unit-masterto lock up or crash entirely without reporting a user-friendly error to the frontend UI.What was changed:
MAX_SAFE_SIZElimit of 100 MB toFileScanUtils.scala.binaryscan loop, if the byte stream exceeds 100 MB, it is immediately intercepted and aborted safely.RuntimeExceptiondirectly to the frontend directing the user to use thelarge binaryattribute type instead for massive files, completely preventing the JVM from thrashing or freezing.Any related issues, documentation, discussions?
Closes #3271
How was this PR tested?
Manual Verification:
File Scanoperator.binaryattribute type (which intentionally attempts to load the entire file into memory).Result: The workflow aborted instantly (in ~3-4 seconds) the moment it crossed the 100MB threshold. It safely threw the custom user-friendly error message in the UI and immediately cleaned up the worker thread without any memory leaks or GC pauses.
Was this PR authored or co-authored using generative AI tooling?
Generated-by: Antigravity (DeepMind)
Changed it to 100 Mb instead of 1gb afterwards
